/*
 * JHistoryTextField.java 
 * 
 * Modified on Aug 6, 2011
 * By Ice Spring.
 */
package com.mrogrape.gui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.DefaultListModel;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.LineBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

/**
 * 一个可以显示历史记录的文本输入域。
 * 
 * <p>
 * JHistoryTextField在内部维护了一个历史记录列表。可以在用户输入时动态的匹配与之
 * 相类似的历史记录并弹出一个列表提供选择。虽然一般用法如此，但它的功能却并不仅限于此。
 * 适当的设置历史记录列表的内容可以简化用户的输入或者用以提示用户。这种类别的文本
 * 输入域你可以从你PC中的浏览器地址栏或资源浏览器地址栏中看到例子。
 * 
 * <p>
 * 默认情况下JHistoryTextField在匹配内容时采用模糊匹配法，即只要历史记录中的某条记录
 * 中含有用户输入的字符串，那么就把它加入到提示列表中。如果需要采用严格匹配法，即完全
 * 匹配显示，请更改StrictMatching属性的值为true。
 * 
 * <p>
 * 此外，默认情况下匹配时大小写敏感，请更改CaseSensitive属性来更改策略。
 * 
 * @author Ice Spring
 * @version 1.0
 */
public final class HistoryTextField extends JTextField {

	/** SerialVersionUID */
	private static final long serialVersionUID = 1L;

	private static final int MAX_VISIBLE_ROWS = 8;

	/**
	 * 构造默认的JHistoryTextField。
	 */
	public HistoryTextField() {

		this(null);
	}

	/**
	 * 构造JHistoryTextField并指定历史记录列表。
	 * 
	 * @param history 指定的历史记录列表。
	 */
	public HistoryTextField(List<String> history) {

		this.initComponents();
		if (history != null && history.size() != 0) {
			this.setHistory(history);
		}
	}

	/**
	 * 向历史记录列表中添加一条记录。
	 * 
	 * @param record 向历史记录列表中添加的记录。
	 */
	public void addRecord(String record){
		this.history.add(record);
	}

	/**
	 * 获取历史记录列表。
	 * 
	 * @return 历史记录列表。
	 */
	public List<String> getHistory() {

		return Collections.unmodifiableList(this.history);
	}

	/**
	 * 设置历史记录列表。
	 * 
	 * @param history 历史记录列表。
	 */
	public void setHistory(List<? extends String> history) {

		this.history.clear();
		this.history.addAll(history);
	}

	/**
	 * 获取最大显示行数。
	 * 
	 * <p>
	 * 这个值指示了候选列表的最大显示行数。
	 * 
	 * @return 最大显示行数。
	 */
	public int getMaxVisableRows() {

		return this.maxVisableRows;
	}

	/**
	 * 设置最大显示行数。
	 * 
	 * @param maxVisableRows 最大显示行数。
	 */
	public void setMaxVisableRows(int maxVisableRows) {

		this.maxVisableRows = maxVisableRows;
	}

	/**
	 * 测试在查询数据时是否采用严格匹配方案。
	 * 
	 * <p>
	 * 在进行输入匹配时是否采用严格匹配方案，默认为false。即采用模糊匹配。
	 * 
	 * @return 是否采用严格匹配的boolean值：
	 *         true：采用严格匹配，
	 *         false：采用模糊匹配。
	 */
	public boolean isStrictMatching() {

		return this.isStrictMatching;
	}

	/**
	 * 设置测试在查询数据时是否采用严格匹配。
	 * 
	 * @param isStrictMatching 是否采用严格匹配的boolean值：
	 *                         true：采用严格匹配，
	 *                         false：采用模糊匹配。
	 */
	public void setStrictMatching(boolean isStrictMatching) {

		this.isStrictMatching = isStrictMatching;
	}

	/**
	 * 进行输入匹配时是否区分大小写。
	 * 
	 * <p>
	 * 默认情况下此选项为true，即区分大小写。
	 * 
	 * @return 指示进行输入匹配时是否区分大小写的boolean值。
	 *         true：大小写敏感，
	 *         false：大小写不敏感。
	 */
	public boolean isCaseSensitive() {

		return this.isCaseSensitive;
	}

	/**
	 * 设置进行输入匹配时是否区分大小写。
	 * 
	 * @param isCaseSensitive 指示进行输入匹配时是否区分大小写的boolean值。
	 *                         true：大小写敏感，
	 *                         false：大小写不敏感。
	 */
	public void setCaseSensitive(boolean isCaseSensitive) {

		this.isCaseSensitive = isCaseSensitive;
	}

	private void initComponents() {

		JScrollPane scrollPane = new JScrollPane(this.list,
				ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
				ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
		scrollPane.setHorizontalScrollBar(null);
		scrollPane.setBorder(null);
		this.list.setFocusable(false);
		this.popup.add(scrollPane);
		this.popup.setFocusable(false);
		this.popup.setBorder(new LineBorder(Color.BLACK, 1));
		this.getDocument().addDocumentListener(new DocumentListener() {
			public void insertUpdate(DocumentEvent e) {

				HistoryTextField.this.onTextChanged();
			}

			public void removeUpdate(DocumentEvent e) {

				HistoryTextField.this.onTextChanged();
			}

			public void changedUpdate(DocumentEvent e) {

				HistoryTextField.this.onTextChanged();
			}
		});
		this.list.addMouseMotionListener(new MouseAdapter() {
			@Override
			public void mouseMoved(MouseEvent e) {

				int index = HistoryTextField.this.list.locationToIndex(e
						.getPoint());
				if (index >= 0
						&& HistoryTextField.this.list.getSelectedIndex() != index) {
					HistoryTextField.this.list.setSelectedIndex(index);
				}
			}
		});
		this.list.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseReleased(MouseEvent e) {

				if (SwingUtilities.isLeftMouseButton(e)) {
					HistoryTextField.this
					.setTextWithoutNotification(HistoryTextField.this.list
							.getSelectedValue().toString());
					HistoryTextField.this.popup.setVisible(false);
				}
			}
		});
		this.addFocusListener(new FocusAdapter() {
			@Override
			public void focusLost(FocusEvent e) {

				HistoryTextField.this.popup.setVisible(false);
			}
		});
		this.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {

				if (HistoryTextField.this.popup.isShowing()) {
					switch (e.getKeyCode()) {
					case KeyEvent.VK_UP: {
						HistoryTextField.this.changeListSelectedIndex(-1);
						break;
					}
					case KeyEvent.VK_PAGE_UP: {
						HistoryTextField.this
						.changeListSelectedIndex(-HistoryTextField.this.list
								.getVisibleRowCount());
						break;
					}
					case KeyEvent.VK_DOWN: {
						HistoryTextField.this.changeListSelectedIndex(1);
						break;
					}
					case KeyEvent.VK_PAGE_DOWN: {
						HistoryTextField.this
						.changeListSelectedIndex(HistoryTextField.this.list
								.getVisibleRowCount());
						break;
					}
					case KeyEvent.VK_ESCAPE: {
						HistoryTextField.this.popup.setVisible(false);
						HistoryTextField.this
						.setTextWithoutNotification(HistoryTextField.this.userText);
						break;
					}
					case KeyEvent.VK_ENTER:
					case KeyEvent.VK_LEFT:
					case KeyEvent.VK_RIGHT: {
						HistoryTextField.this.popup.setVisible(false);
						break;
					}
					}
				} else {
					if (e.getKeyCode() == KeyEvent.VK_DOWN
							|| e.getKeyCode() == KeyEvent.VK_UP
							|| e.getKeyCode() == KeyEvent.VK_PAGE_UP
							|| e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
						HistoryTextField.this.userText = HistoryTextField.this
						.getText();
						HistoryTextField.this.showFilteredHistory();
					}
				}
			}
		});
	}

	private void changeListSelectedIndex(int delta) {

		int size = this.list.getModel().getSize();
		int index = this.list.getSelectedIndex();
		int newIndex;
		if (index < 0) {
			newIndex = delta > 0 ? 0 : size - 1;
		} else {
			newIndex = index + delta;
		}
		if (newIndex >= size || newIndex < 0) {
			newIndex = newIndex < 0 ? 0 : size - 1;
			if (index == newIndex) {
				newIndex = -1;
			}
		}
		if (newIndex < 0) {
			this.list.getSelectionModel().clearSelection();
			this.list.ensureIndexIsVisible(0);
			this.setTextWithoutNotification(this.userText);
		} else {
			this.list.setSelectedIndex(newIndex);
			this.list.ensureIndexIsVisible(newIndex);
			this.setTextWithoutNotification(this.list.getSelectedValue().toString());
		}
	}

	private void setTextWithoutNotification(String text) {

		this.notificationDenied = true;
		try {
			this.setText(text);
		} finally {
			this.notificationDenied = false;
		}
	}

	private void onTextChanged() {

		if (!this.notificationDenied) {
			this.userText = this.getText();
			this.showFilteredHistory();
		}
	}


	private void showFilteredHistory() {

		this.list.getSelectionModel().clearSelection();
		DefaultListModel model = (DefaultListModel) this.list
		.getModel();
		model.clear();
		for (String s : this.history) {
			if (this.isStrictMatching && this.isCaseSensitive) {
				if (s.startsWith(this.userText)) {
					model.addElement(s);
				}
			} else if(!this.isStrictMatching && this.isCaseSensitive){
				if (s.contains(this.userText)) {
					model.addElement(s);
				}
			} else if(this.isStrictMatching && !this.isCaseSensitive){
				if (s.toUpperCase().startsWith(this.userText.toUpperCase())) {
					model.addElement(s);
				}
			} else if(!this.isStrictMatching && !this.isCaseSensitive){
				if (s.toUpperCase().contains(this.userText.toUpperCase())) {
					model.addElement(s);
				}
			}
		}
		int size = model.size();
		if (size == 0) {
			this.popup.setVisible(false);
		} else {
			this.list.setVisibleRowCount(size < this.getMaxVisableRows() ? size
					: this.getMaxVisableRows());
			this.popup.pack();
			if (!this.popup.isShowing()) {
				this.popup.show(HistoryTextField.this, 0, this.getHeight());
			}
		}
	}


	// *********************************************************************
	// Properties
	// *********************************************************************
	private int maxVisableRows = HistoryTextField.MAX_VISIBLE_ROWS;
	private boolean isStrictMatching = false;
	private boolean isCaseSensitive = true;
	private final List<String> history = new ArrayList<String>();

	// *********************************************************************
	// Local Variables
	// *********************************************************************
	private String userText;
	private boolean notificationDenied;

	// *********************************************************************
	// Swing Components
	// *********************************************************************
	private final JPopupMenu popup = new JPopupMenu() {
		/** SerialVersionUID */
		private static final long serialVersionUID = 1L;


		@Override
		public Dimension getPreferredSize() {

			Dimension dimension = super.getPreferredSize();
			dimension.width = HistoryTextField.this.getWidth();
			return dimension;
		}
	};


	private final JList list = new JList(new DefaultListModel());
}
